package com.google.code.stalkernet.chat.ui.model;

import java.io.ByteArrayInputStream;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.Executor;

import javax.swing.SwingUtilities;
import javax.swing.table.AbstractTableModel;

import com.google.code.stalkernet.database.Database;
import com.google.code.stalkernet.database.event.DatabaseEvent;
import com.google.code.stalkernet.database.event.DatabaseListener;
import com.google.code.stalkernet.entry.Entry;
import com.google.code.stalkernet.entry.EntryID;

/**
 * Model for chat table.
 *
 * @author Mikhail Vladimirov
 */
public class ChatTableModel extends AbstractTableModel
{
    private final DatabaseListener databaseListener = new MyDatabaseListener ();

    private final List <Entry> entries = new ArrayList <> ();

    /**
     * Create chat table model.
     *
     * @param database {@link Database} to use
     * @param executor {@link Exception} to use for database-related operations
     */
    public ChatTableModel (Database database, Executor executor)
    {
        if (database == null)
            throw new IllegalArgumentException ("Database is null");

        if (executor == null)
            throw new IllegalArgumentException ("Executor is null");

        executor.execute (new DumpDatabaseRunnable (database));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getRowCount ()
    {
        return entries.size ();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getColumnCount ()
    {
        return 3;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object getValueAt (int rowIndex, int columnIndex)
    {
        if (rowIndex < 0)
            throw new IllegalArgumentException (
                "Row index (" + rowIndex + ") < 0");

        int entitiesCount = entries.size ();

        if (rowIndex >= entitiesCount)
            throw new IllegalStateException (
                "Row index (" + rowIndex + ") >= entities count (" +
                entitiesCount + ")");

        Entry entry = entries.get (rowIndex);

        switch (columnIndex)
        {
        case 0:
            return getTimeFromEntry (entry);
        case 1:
            return getAuthorFromEntry (entry);
        case 2:
            return getMessageFromEntry (entry);
        default:
            throw new IllegalArgumentException (
                "Invalid column index: " + columnIndex);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getColumnName (int column)
    {
        switch (column)
        {
        case 0:
            return "Time";
        case 1:
            return "Author";
        case 2:
            return "Message";
        default:
            throw new IllegalArgumentException ("Invalid column: " + column);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Class <?> getColumnClass (int columnIndex)
    {
        switch (columnIndex)
        {
        case 0:
            return Date.class;
        case 1:
            return String.class;
        case 2:
            return String.class;
        default:
            throw new IllegalArgumentException (
                "Invalid column: " + columnIndex);
        }
    }

    private void addEntry (Entry entry)
    {
        SwingUtilities.invokeLater (new AddEntryRunnable (entry));
    }

    private Date getTimeFromEntry (Entry entry)
    {
        if (entry == null)
            throw new IllegalArgumentException ("Entry is null");

        try
        {
            return new Date (
                new DataInputStream (
                    new ByteArrayInputStream (
                        entry.getBody ().getBytes ())).readLong ());
        }
        catch (EOFException ex)
        {
            throw new IllegalArgumentException ("Invalid entry: " + entry);
        }
        catch (IOException ex)
        {
            throw new Error ("Impossible", ex);
        }
    }

    private String getAuthorFromEntry (Entry entry)
    {
        if (entry == null)
            throw new IllegalArgumentException ("Entry is null");

        try
        {
            return new DataInputStream (
                new ByteArrayInputStream (
                    entry.getBody ().getBytes (),
                    8, Integer.MAX_VALUE - 8)).readUTF ();
        }
        catch (EOFException ex)
        {
            throw new IllegalArgumentException ("Invalid entry: " + entry);
        }
        catch (IOException ex)
        {
            throw new Error ("Impossible", ex);
        }
    }

    private String getMessageFromEntry (Entry entry)
    {
        if (entry == null)
            throw new IllegalArgumentException ("Entry is null");

        try
        {
            DataInput dataInput = new DataInputStream (
                new ByteArrayInputStream (
                    entry.getBody ().getBytes (),
                    8, Integer.MAX_VALUE - 8));

            dataInput.readUTF (); // Skip author

            return dataInput.readUTF ();
        }
        catch (EOFException ex)
        {
            throw new IllegalArgumentException ("Invalid entry: " + entry);
        }
        catch (IOException ex)
        {
            throw new Error ("Impossible", ex);
        }
    }

    private class DumpDatabaseRunnable implements Runnable
    {
        private final Database database;

        private DumpDatabaseRunnable (Database database)
        {
            if (database == null)
                throw new IllegalArgumentException ("Database is null");

            this.database = database;
        }

        @Override
        public void run ()
        {
            Set <EntryID> entryIDs = new HashSet <> (
                Arrays.asList (database.getLeafIDs ()));

            Queue <EntryID> queue = new ArrayDeque <> (entryIDs);

            EntryID entryID;

            while ((entryID = queue.poll ()) != null)
            {
                Entry entry = database.getEntry (entryID);
                addEntry (entry);

                for (EntryID parentID: entry.getParentIDs ())
                {
                    if (entryIDs.add (parentID))
                        queue.add (parentID);
                }
            }

            database.addDatabaseListener (databaseListener);
        }
    }

    private class AddEntryRunnable implements Runnable
    {
        private final Entry entry;

        public AddEntryRunnable (Entry entry)
        {
            if (entry == null)
                throw new IllegalArgumentException ("Entry is null");

            this.entry = entry;
        }

        @Override
        public void run ()
        {
            int index = entries.size ();
            entries.add (entry);
            fireTableRowsInserted (index, index);
        }
    }

    private class MyDatabaseListener implements DatabaseListener
    {
        @Override
        public void entryAdded (DatabaseEvent databaseEvent)
        {
            if (databaseEvent == null)
                throw new IllegalArgumentException ("Database event is null");

            addEntry (databaseEvent.getEntry ());
        }
    }
}
